package com.zzyl.nursing.service.impl;

import cn.hutool.core.bean.BeanUtil;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.huaweicloud.sdk.iotda.v5.model.AuthInfo;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.huaweicloud.sdk.iotda.v5.IoTDAClient;
import com.huaweicloud.sdk.iotda.v5.model.*;
import com.zzyl.common.constant.CacheConstants;
import com.zzyl.common.core.domain.AjaxResult;
import com.zzyl.common.exception.base.BaseException;
import com.zzyl.common.utils.StringUtils;
import com.zzyl.nursing.dto.DeviceDto;
import com.zzyl.nursing.util.DateTimeZoneConverter;
import com.zzyl.nursing.vo.DeviceDataVo;
import com.zzyl.nursing.vo.DeviceDetailVo;
import com.zzyl.nursing.vo.ProductVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.domain.Device;
import com.zzyl.nursing.service.IDeviceService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
 * 设备管理Service业务层处理
 * 
 * @author Nan
 * @date 2025-06-01
 */
@Service
public class DeviceServiceImpl extends ServiceImpl<DeviceMapper,Device> implements IDeviceService
{
    @Autowired
    private DeviceMapper deviceMapper;

    // 注入华为云平台对象
    @Autowired
    private IoTDAClient ioTDAClient;

    // 注入redis
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 查询产品详情
     * @param productKey
     * @return
     */
    @Override
    public AjaxResult queryProduct(String productKey) {
        //参数校验
        if(StrUtil.isEmpty(productKey)){
            throw new BaseException("请输入正确的参数");
        }
        //调用华为云物联网接口
        ShowProductRequest showProductRequest = new ShowProductRequest();
        showProductRequest.setProductId(productKey);
        ShowProductResponse response;

        try {
            response = ioTDAClient.showProduct(showProductRequest);
        } catch (Exception e) {
            throw new BaseException("查询产品详情失败");
        }
        //判断是否存在服务数据
        List<ServiceCapability> serviceCapabilities = response.getServiceCapabilities();
        if(CollUtil.isEmpty(serviceCapabilities)){
            return AjaxResult.success(Collections.emptyList());
        }

        return AjaxResult.success(serviceCapabilities);
    }

    /**
     *
     * @param iotId // 根据设备id 删除指定设备
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delDeviceByItoId(String iotId) {

    //     1.先从iot 中删除，然后再 从我们表中删除
        DeleteDeviceRequest request = new DeleteDeviceRequest();
        request.setDeviceId(iotId);
        DeleteDeviceResponse deleteDeviceResponse = ioTDAClient.deleteDevice(request);

    //     2.获取删除状态
        int httpStatusCode = deleteDeviceResponse.getHttpStatusCode();

        if(httpStatusCode!=204){
            throw new BaseException("删除iot数据失败");
        }

    //     3. 删除成功后 就删除我们表中的数据
        try {
            LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Device::getIotId,iotId);
            this.remove(queryWrapper);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }


    }


    /**
     *
     * @param dto // 要修改的数据
     */
    @Override
    @Transactional( rollbackFor = Exception.class)
    public void updateDevice(DeviceDto dto) {
        System.out.println("dto = " + dto);
        /* 思路：修改之前先判断要修改的 设备位置，再我们表中存不存再，因为同1个位置只能放1个同类型的产品
            1. 先修改 iot
            2. 再修改我们表中的数据
        * */

        LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
        //  主要是 3个字段，bindingLocation,locationType,remark 但是device设备表中没有 bindingLocation 这个字段。所以用剩余2个判断即可
        queryWrapper.eq(Device::getLocationType,dto.getLocationType());
        queryWrapper.eq(Device::getRemark,dto.getRemark());

        Device deviceOne = deviceMapper.selectOne(queryWrapper);

       if(JSONUtil.isNull(deviceOne)){
           // 先修改 平台中的数据
           UpdateDeviceRequest request = new UpdateDeviceRequest();
           // 写这个是因为 我们更新内容 要借助setBody（）方法，但是setBody方法 要有个UpdateDevice 类型的参数
           UpdateDevice body = new UpdateDevice();
           body.setDeviceName(dto.getDeviceName());
           body.setDescription(dto.getDeviceDescription());
            request.setBody(body);
           //  设备id,没有会报错
           request.setDeviceId(dto.getIotId());
           UpdateDeviceResponse updateDeviceResponse = ioTDAClient.updateDevice(request);

           int httpStatusCode = updateDeviceResponse.getHttpStatusCode();
           if(httpStatusCode!=200){
               throw new BaseException("修改IOT设备数据失败");
           }


           Device device = new Device();
           device.setId(dto.getId());
           device.setIotId(dto.getIotId());
           // 密钥，修改一次，产生1个新的密钥
           // 密钥，我们自己给他生成,随机生成1个32位字符串
           // String secret = RandomUtil.randomString(32);
           // device.setSecret(secret);
           // 位置
           device.setBindingLocation(dto.getBindingLocation().toString());
           // 设备类型
           device.setLocationType(dto.getLocationType());
           device.setPhysicalLocationType(dto.getPhysicalLocationType());
           device.setDeviceName(dto.getDeviceName());
           device.setProductKey(dto.getProductKey());
           device.setProductName(dto.getProductName());
           device.setDeviceDescription(dto.getDeviceDescription());
           device.setNodeId(dto.getNodeId());
           device.setRemark(dto.getRemark());
           try {
               this.updateById(device);
           } catch (Exception e) {
               e.printStackTrace();
               throw new BaseException("修改失败");
           }

       } else {
           throw new BaseException("同一个位置绑定了相同的产品");
       }




    }




    /**
     *  查看设备上数据的方法
     * @param iotId 设备id
     * @return
     */
    @Override
    public List<DeviceDataVo> queryServiceProperties(String iotId) {
        /* 1. 从iot 上 获取数据，showXxxShadow() 获取到的数据是 1个 数组，数组里面的每一项是1个设备中的1个属性，每个属性中的字段位，
        对象里面的functionId 是 我们的key,value 是 key 对应的值，eventTime 位激活时间
        * */
        //  存储列表数据的list
        ArrayList<DeviceDataVo> list = new ArrayList<>();

        ShowDeviceShadowRequest request = new ShowDeviceShadowRequest();
        request.setDeviceId(iotId);
        ShowDeviceShadowResponse showDeviceShadowResponse = ioTDAClient.showDeviceShadow(request);
        // 判断是否成功
        int httpStatusCode = showDeviceShadowResponse.getHttpStatusCode();
        
        if(httpStatusCode!=200){
            throw new BaseException("IOT设备查询数据失败");
        }
        
        // 查询成功了，获取数据,得到的是1个数组
        List<DeviceShadowData> shadowList = showDeviceShadowResponse.getShadow();

        // 如果返回的数据中 有数据，我们开始从里面获取到我们要的数据
        if(CollUtil.isNotEmpty(shadowList)){

            DeviceShadowData deviceShadowData = shadowList.get(0);
            //  数据存在 reported属性中，是对象
            DeviceShadowProperties reported = deviceShadowData.getReported();
            System.out.println("reported = " + reported);
            //  激活时间 20250601 T 08 24 34 Z
            String eventTimeStr = reported.getEventTime();
            System.out.println("eventTimeStr = " + eventTimeStr);
            //  把字符串时间转为页面需要的
            LocalDateTime eventTimeUTC = LocalDateTimeUtil.parse(eventTimeStr,"yyyyMMdd'T'HHmmss'Z'");
        //     转为页面需要的
            LocalDateTime eventTime =  DateTimeZoneConverter.utcToShanghai(eventTimeUTC);
        //     循环遍历把key 和 value 得到,但是对象不能forEach,不过hutool工具包的 jsonObject可以

            Object properties = reported.getProperties();

            JSONObject jsonObject = JSONUtil.parseObj(properties);

            jsonObject.forEach((key,value)->{
                DeviceDataVo deviceDataVo = new DeviceDataVo();
                deviceDataVo.setEventTime(eventTime);
                deviceDataVo.setFunctionId(key);
                deviceDataVo.setValue(value);
                list.add(deviceDataVo);
            });

        }

        return list;
    }



    /**
     *  查询设备信息的方法
     * @param iotId 设备id
     * @return
     */
    @Override
    public DeviceDetailVo deviceDetail(String iotId) {
        // 页面展示的数据: iot 中的数据+ 我们表中的数据，所以我们要 查两次，1次iot 1次我们自己的表中
        // 查看详细数据之前记得激活设备，把我们设备的id和密钥，拿到激活的方法中，先激活一下
        // 1.从 iot 中查数据
        ShowDeviceRequest request = new ShowDeviceRequest();
        request.setDeviceId(iotId);
        ShowDeviceResponse showDeviceResponse = ioTDAClient.showDevice(request);
        // 2. 判断是否查询成功
        int httpStatusCode = showDeviceResponse.getHttpStatusCode();
        if(httpStatusCode!=200){
            throw new BaseException("IOT设备查询失败");
        }

        // 2.2 本地查询
        LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Device::getIotId,iotId);
        Device device = this.getOne(queryWrapper);


        // 2.3 把本地查到的数据 先copy到 vo中
        DeviceDetailVo deviceDetailVo = BeanUtil.toBean(device, DeviceDetailVo.class);



        // 3. 查询成功，从iot 中 获取我们需要的信息,激活时间，设备状态
        String activeTimeStr = showDeviceResponse.getActiveTime() ;
        // 4. 修改时间格式，这里是 字符串，先把字符串类型的时间，转为LocalDateTime,然后再转为页面需要的格式
        // 4.1 把字符串时间，转为指定格式的时间，糊涂工具包LocalDateTimeUtil.parse（参1，参2） 参数1是 字符串时间，参2是我们要转成的格式
        // UTC_MS_PATTERN 是国际时间

        LocalDateTime  activeTimeParse = LocalDateTimeUtil.parse(activeTimeStr, DatePattern.UTC_MS_PATTERN);
        // 4.2 转为 页面需要的时间格式
        LocalDateTime activeTime = DateTimeZoneConverter.utcToShanghai(activeTimeParse);
        //  4.3 把数据存到激活时间 vo 中
        deviceDetailVo.setActiveTime(activeTime);

        // 4.4 把iot 中的状态存到 vo中
        String status = showDeviceResponse.getStatus();

        deviceDetailVo.setDeviceStatus(status);

        return deviceDetailVo;
    }



    /**
     *
     * @param dto 新增设备的dto 数据
     */
    @Override
    public void register(DeviceDto dto) {
    /* 业务逻辑：
        1. 新增之前 先校验数据：
            1， 设备标识受否重复。
            2.  设备名称是否重复
            3. 同1位置处 是否有了相同的设备
        2. 校验成功后：
            1. 向iot 中 添加数据
            2. 向我们表中添加数据
    * */

    //     1，校验
        LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Device::getNodeId,dto.getNodeId());

        // count() 返回 查询得到的数量，如果>0 说明此时表中有当前这条数据，抛错误
        if(this.count(queryWrapper)>0){
            throw new BaseException("设备标识重复");
        }

        queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(Device::getDeviceName,dto.getDeviceName());

        // count() 返回 查询得到的数量，如果>0 说明此时表中有当前这条数据，抛错误
        if(this.count(queryWrapper)>0){
            throw new BaseException("设备名称重复");
        }

    //     查看同1位置处 是否有了相同的设备
    //     相同的设备： 设备类型，位置，产品key 这3条件都一样才是同1位置有了相同的数据
        queryWrapper = new LambdaQueryWrapper<>();
        // 设备类型locationType
        queryWrapper.eq(Device::getLocationType,dto.getLocationType());
    //     具体位置bindingLocation
        queryWrapper.eq(Device::getBindingLocation,dto.getBindingLocation());
    //     产品的key
        queryWrapper.eq(Device::getProductKey,dto.getProductKey());

        if(this.count(queryWrapper)>0){
            throw new BaseException("同1位置已经绑定了相同的产品");
        }

    //     开始向 iot 中 添加 设备
        AddDeviceRequest request = new AddDeviceRequest();

        AddDevice body = new AddDevice();

        // body.setDeviceId(); //iot 字段是 iot 自动生成的

        body.setNodeId(dto.getNodeId());

        body.setDeviceName(dto.getDeviceName());

        body.setProductId(dto.getProductKey());

        // 密钥，我们自己给他生成,随机生成1个32位字符串
        AuthInfo authInfo = new AuthInfo();
        String secret = RandomUtil.randomString(32);
        authInfo.setSecret(secret);
        // 密钥
        body.setAuthInfo(authInfo);

        body.setDescription(dto.getDeviceDescription());

        request.setBody(body);

        AddDeviceResponse addDeviceResponse = ioTDAClient.addDevice(request);

    //     判断向iot 中 新增是否成功，新增成功的状态码是201
        int httpStatusCode = addDeviceResponse.getHttpStatusCode();
        if(httpStatusCode!=201){
           throw new BaseException("IOT设备添加失败");
        }
    //     添加成功之后，会返回设备id
        String deviceId = addDeviceResponse.getDeviceId();

    //     把新增的数据存到我们表中
        // 我们表中存的数据，比 dto 新增的数据多了，设备id,密钥，这两个字段
        // 糊涂工具包的   BeanUtil.toBean(参1，参2的类型)  参1是要复制的数据，参2是 要复制到其他类的 类型，可以之间把参数1复制1份
        Device device = BeanUtil.toBean(dto, Device.class);
        // 密钥
        device.setSecret(secret);
        // 设备id
        device.setIotId(deviceId);
        this.save(device);
    }



    /**
     *  查询iot 平台中的所有产品
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void syncProductList() {

        ListProductsRequest request = new ListProductsRequest();
        request.setLimit(50);
        //  ioTDAClient.listProducts（ListProductsRequest类型的对象）获取所有的产品列表
        ListProductsResponse response = ioTDAClient.listProducts(request);

    // getHttpStatusCode() 获取状态码 如果状态码不为200 说明查询失败了，要抛出异常
        if(response.getHttpStatusCode()!=200){
          throw new BaseException("IOT数据同步失败");
        }

    //     同步成功，把数据存到redis中
    //     getProducts（）获取产品列表
        List<ProductSummary> products = response.getProducts();
        String jsonStr = JSONUtil.toJsonStr(products);
        //     存到redis中
        redisTemplate.boundValueOps(CacheConstants.IOT_ALL_PRODUCT_LIST).set(jsonStr);

    }

    /**
     *
     * @return // 从redis 中 获取产品列表的数据
     */
    @Override
    public List<ProductVo> allProduct() {

        String s = redisTemplate.boundValueOps(CacheConstants.IOT_ALL_PRODUCT_LIST).get();

        // 如果有值，把内容转为 json对象,并返回，如果没有值 返回空数组

        if(s!=null){

            //  JSONUtil.toList(参1, 参2) 可以把参1 转为数组，参数1中的每一项是参数2类型
            List<ProductVo> list = JSONUtil.toList(s, ProductVo.class);

            return list;
        }
        //  List.of() 返回1个空数组

        return List.of();
    }



    /**
     * 查询设备管理列表
     * 
     * @param device 设备管理
     * @return 设备管理
     */
    @Override
    public List<Device> selectDeviceList(Device device)
    {
        return deviceMapper.selectDeviceList(device);
    }




}
